<?php
/**
 * qTemplateParser is easy-extendable template parser. Some example templates and use here soon (tm).
 */
class qTemplateParser 
{
    /** Registered variables. */
    private static $values = array();
    /** Registered subkeys, for use mainly in foreach handler. */
    private static $subkeys = array();
    /** Registered handlers. */
    private static $handlers = array();
    
    /**
     * Initializes parser by loading basic handlers: if, switch, for, while, foreach, include, bbcode.
     */
    public static function Init()
    {
        self::InstallHandler("if");
        self::InstallHandler("switch");
        self::InstallHandler("for");
        self::InstallHandler("while");
        self::InstallHandler("foreach");
        self::InstallHandler("include");
        self::InstallHandler("bbcode");
    }
    
    /**
     * Loads handler. Handler's source must be located in lib_path/templates/handlers and named q-handlername-.handler.php.
     * @param string $handler handler's name
     * @return boolean
     */
    public static function InstallHandler($handler)
    {   
        // return true if it is already installed
        if (self::IsHandlerInstalled($handler))
        {
            return true;
        }
        
        // include file
        include lib_path."templates/handlers/$handler.handler.php";
        // construct class name and object
        $handlerclass = "q{$handler}Handler";
        $handlerinst = @new $handlerclass;
        
        // if handler does not derive from qHandler, then return false
        if (!($handlerinst instanceof qHandler))
        {
            return false;
        }
        
        // add handler as registered one and return true
        self::$handlers[$handler] = $handlerinst;
        return true;
    }
    
    /**
     * Registers variable.
     * @param string $key name of variable
     * @param mixed $value value of variable - mixed or array
     */
    public static function AddValue($key, $value)
    {
        // if value is array...
        if (is_array($value))
        {
            // ...then for each of elem, register it's value with this function and register subkey
            foreach ($value as $subkey => $subvalue)
            {
            	self::AddValue($key.'.'.$subkey, $subvalue);
                self::$subkeys[$key][] = $key.'.'.$subkey;
            }
        }

        // else simply add value to $values
        else
        {
            self::$values[$key] = $value;
        }
    }
    
    /**
     * Returns subkeys.
     * @return Subkeys array.
     */
    public static function Subkeys()
    {
        return self::$subkeys;
    }
    
    /**
     * Returns values.
     * @return Values array.
     */
    public static function Values()
    {
        return self::$values;
    }
    
    /**
     * Checks if handler is already installed.
     * @param string $handler handler's name
     * @return boolean 
     */
    public static function IsHandlerInstalled($handler)
    {
        return isset(self::$handlers[$handler]);
    }
    
    /**
     * Checks if handler needs closing tag.
     * @param string $handler handler name
     * @return boolean / null
     */
    private static function IsCloseable($handler)
    {
    	if (isset(self::$handlers[$handler]))
    	{
            return self::$handlers[$handler]->IsCloseable();
    	}
    	else
    	{
    	    return null;
    	}
    }
    
    /** 
     * Check if tag wants parser to parse his children or not.
     * @param string $handler handler name
     * @return boolean / null 
     */
    private static function ParseChildren($handler)
    {
    	if (isset(self::$handlers[$handler]))
    	{
            return self::$handlers[$handler]->ParseChildren();
    	}
    	else
    	{
    	    return null;
    	}
    }
    
    /**
     * Parses controller, using all of it's variables and template filename.
     * @param string $controller controller object
     */
    public static function ParseController($controller)
    {
    	foreach ($controller->GetVariables() as $key => $value)
    	{
    	    self::AddValue($key, $value);
    	}

    	self::ParseFile($controller->GetTemplateFilename());   
    }
    
    /** 
     * Parses template file named $filename.
     * @param string $filename filename of template file
     */
    public static function ParseFile($filename)
    {
        // TODO
    }
    
    /**
     * Parses template from $text.
     * @param string $text template to be parsed.
     * @return Parsed string. 
     */
    public static function Parse($text)
    {
    	$tree = self::_create_tree($text);
    	$text = self::_parse_tree($tree, $text);
    	$text = self::_parse_values($text);
    	return $text;	
    }
    
    /**
     * Replaces variables' keys with it's values.
     * @param string $text template string
     * @return Parsed string.
     */
    private static function _parse_values($text)
    {
        foreach (self::$values as $key => $value)
        {
            $text = str_replace('$'.$key, $value, $text);
        }
        return $text;
    }
    
    /**
     * Walks through tree and parses all tags.
     * @param array $tree tree built by _create_tree()
     * @param string $text template to be parsed
     * @return Parsed string. 
     */
    private static function _parse_tree($tree, $text)
    {
        // parse all tree nodes...
        foreach ($tree as $node)
        {
            // parse children, if any, using this function - most of children are parsed by tag itself, so this is not common case
            if ($node['children'] != false)
            {
            	$node['body'] = self::_parse_tree($node['children'], $node['body']);
            }
            // parse tag and replace it with parsed one in template string
            $newval = self::_parse_tag($node);
            $text = str_replace($node['entire'], $newval[1], $text);
        }
        
        return $text;
    }
    
    /**
     * Calls handler to parse tag.
     * @param array $tag tag data
     * @return Parsed string. 
     */
    private static function _parse_tag($tag)
    {
        return self::$handlers[$tag['id']]->Parse($tag['tag'], $tag['body']);
    }

    /**
     * Creates tree array from text.
     * @param string $text template to be parsed.
     * @return Tree in form of array / false if string is empty.
     */
    private static function _create_tree($text)
    {
        // current position is -1...
        $pos = -1;
        // positions of { and } are 0...
        $tmppos1 = $tmppos2 = 0;
        // lenght of text is...
        $strlen = mb_strlen($text);
 
        // if string is empty, return false
        if ($strlen == 0) return false;

        // initialize tree
        $tree = array();

        // let's parse this crap into tree array...
        while ($pos < $strlen)
        {
            // find first {, probably there a tag starts
            $pos = mb_strpos($text, "{", $pos + 1);

            // there is no tag; why are we parsing this?
            if ($pos === false)
            {
                return $tree;
            }
            
            // there is a tag... what a pity
            else
            {
                // let's find next {...
            	$tmppos1 = mb_strpos($text, "{", $pos + 1);
                // and next }
                $tmppos2 = mb_strpos($text, "}", $pos + 1);
                
                // if next { occurs before next }, then we need to skip current one
                if ($tmppos1 < $tmppos2)
                {
                    continue;
                }
                
                // let's get everything between { and }...
                $string = mb_substr($text, $pos, $tmppos2 - $pos + 1);
                // let's find space position
                $spacepos = mb_strpos($string, " ", $pos + 1);
                // if it's outside tag... let's say it's inside - why? to parse this one, dude, look at next comment
                // and spacepos must be len, so lest do $spacepos (or closingtagpos) - $pos
                if ($spacepos === false || $spacepos > $tmppos2) 
                {
                    $spacepos = $tmppos2 - $pos;
                }
                else
                {
                    $spacepos = $spacepos - $pos;
                }
                // command, or tag name, or handler name, finishes at space position or at closing tag
                $cmd = mb_substr($string, 1, $spacepos - 1);
                
                // if tag is not recognized, let's try next one
                if (!self::IsHandlerInstalled($cmd))
                {
                    continue;
                }
                
                // if tag does not want to be closed, let's add it to tree and proceed to next one
                else if (!self::IsCloseable($cmd))
                {
                    $tree[] = array(
                    	'id' => $cmd,
                    	'tag' => $string
                    );
                    continue;
                }
                
                // but when tag is installed and does want to be closed... damn, there is much to be done now :<
                else
                {
                    // first, let's find closing tag... and let's call another method to do the worst work ever
                    $closing = self::_find_closing_tag($cmd, mb_substr($text, $tmppos2 + 1));
                    
                    // if that function found closing one, let's build a tree entry
                    if ($closing !== false)
                    {
                        // here it is, new tree node...
 	                $elem = array(
                            'id' => $cmd,
                            'tag' => $string
            	        );
            	        
                        // let's get body of tag, it's from closing bracket of opening tag to opening bracket of closing tag
            	        $elem['body'] = mb_substr($text, $tmppos2 + 1, $closing[0] - $pos);
                        // and entire tag, that means opening tag + body + closing tag
            	        $elem['entire'] = mb_substr($text, $pos, $tmppos2 + $closing[1] - $pos + 2);
                        
                        // if tag wants me to parse it's children, then let's do this... uhm, i'm calling myself...
            	        if (self::ParseChildren($cmd))
            	        {
                            $elem['children'] = self::_create_tree($elem['body']);
            	        }
                        
                        // otherwise it will probably parse them by itself, calling me in some point, what a pity
            	        else
            	        {
            	            $elem['children'] = false;
            	        }
                        // let's finally add node to tree
            	        $tree[] = $elem;
            	        
                        // and let's jump after closing bracket of closing tag
            	        $pos = $tmppos2 + $closing[1];
                        // and do the job again...
            	        continue;
                    }
                               
                    // otherwise, lt's try next tag
                    else
                    {
                    	$pos++;
                        continue;
                    }
                }
            }
        }
        
        return $tree;
    }
    
    /**
     * Finds closing tag of given tag.
     * @param string $tag given tag
     * @param string $text template part to be searched
     * @return array ( opening bracket position, tag len )
     */
    private static function _find_closing_tag($tag, $text)
    {
        // let's build stack and push $tag on it
    	$stack = new qStack();
    	$stack->Push($tag);

        // let's get lenght of text and set position to -1
    	$len = mb_strlen($text);
    	$pos = -1;
    	
        // while there are some characters after position and stack is not empty...
    	while ($pos < $len && !($stack->IsEmpty()))
    	{
            // let's find opening bracket...
    	    $pos = mb_strpos($text, "{", $pos + 1);
    	    
            // nothing found, let's return false
    	    if ($pos === false)
    	    {
   	        return false;
    	    }
    	    
            // uhm, something found, let's do the job
            else
            {
                // check prievous function for documentation of this part...
            	$tmppos1 = mb_strpos($text, "{", $pos + 1);
                $tmppos2 = mb_strpos($text, "}", $pos + 1);
                
                if ($tmppos1 < $tmppos2 && $tmppos1 !== false)
                {
                    continue;
                }
                
                $string = mb_substr($text, $pos, $tmppos2 - $pos + 1);
                $spacepos = mb_strpos($string, " ");
                if ($spacepos === false || $spacepos > $tmppos2) 
                {
                    $spacepos = $tmppos2 - $pos;
                }
                else
                {
                    $spacepos = $spacepos - $pos;
                }
                $cmd = mb_substr($string, 1, $spacepos - 1);
                
                // skip empty tags
                if ($cmd == '')
                {
                    continue;
                }
                
                // if tag is not recognized, then skip it
                else if (!(self::IsHandlerInstalled($cmd) || self::IsHandlerInstalled(mb_substr($cmd, 3))))
                {
                    continue;
                }
                
                // if tag is not closeable, then skip it
                else if (!(self::IsCloseable($cmd)))
                {
                    continue;
                }    	    
                
                // oh, it's interesting
                else
                {
                    // if we found closing tag of last put on stack... pop it!
                    $last = $stack->Check();
                    if ($last == mb_substr($cmd, 3))
                    {
                        $stack->Pop();
                    }
                    
                    // otherwise push it
                    else if (self::IsHandlerInstalled($cmd))
                    {
                        $stack->Push($cmd);
                    }
                }
            }
    	}
    	
        // clear stack and return position and lenght
    	$stack->Clear();
        return array($pos, $tmppos2 - $pos);
    }
}